/*
* Copyright (c) 2012 Kyle Hayward
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#pragma once

#include "BufferStream.h"

namespace OptixDotNet 
{
	ref class TextureSampler;

	/// <summary>
	/// Specifies the direction of data flow from the host to the OptiX devices.
	/// </summary>
	public enum class BufferType
	{
		/// <summary>
		/// Invalid BufferType used for error catching.
		/// </summary>
		None				= 0,

		/// <summary>
		/// Buffer will provide data input to OptiX devices. Only the host can write data to the buffer.
		/// </summary>
		Input				= RT_BUFFER_INPUT,

		/// <summary>
		/// Buffer will provide data output to the host. Only the OptiX device can write data to the buffer.
		/// </summary>
		Output				= RT_BUFFER_OUTPUT,

		/// <summary>
		/// Buffer will provide data output to the host and data input to OptiX devices. 
		/// Data can be read and written by both host and OptiX device.
		/// </summary>
		InputOutput			= RT_BUFFER_INPUT_OUTPUT,

		/// <summary>
		/// Buffer will provide the host to write data, and the OptiX device to read and write data. 
		/// The buffer will never be copied back to the host. A seperate copy lives on each OptiX device.<br/>
		/// Useful for accumulating intermediate results on OptiX devices in multi-gpu environments.
		/// </summary>
		InputOutputLocal	= RT_BUFFER_INPUT_OUTPUT | RT_BUFFER_GPU_LOCAL
	};

	/// <summary>
	/// Description data used to create a <see cref="OptixDotNet::Buffer">Buffer</see>.
	/// </summary>
	public value struct BufferDesc
	{
		/// <summary>
		/// Width of the Buffer.
		/// </summary>
		Size Width;

		/// <summary>
		/// Height of the Buffer.
		/// </summary>
		Size Height;

		/// <summary>
		/// Depth of the Buffer.
		/// </summary>
		Size Depth;

		/// <summary>
		/// Size of the Type the buffer contains. Only needed for <see cref="OptixDotNet::Format::User">Format.User</see> formats.
		/// </summary>
		Size ElemSize;

		/// <summary>
		/// <see cref="OptixDotNet::BufferType">Type</see> of the Buffer.
		/// </summary>
		BufferType Type;

		/// <summary>
		/// <see cref="OptixDotNet::Format">Format</see> of the Buffer
		/// </summary>
		OptixDotNet::Format Format;

		/// <summary>
		/// Gets a default BufferDesc with safe initilaized values.
		/// Width = 1
		/// Height = 1
		/// Depth = 1
		/// ElemSize = 0
		/// Type = None
		/// Format = Unknown
		/// </summary>
		static property BufferDesc Default
		{
			BufferDesc get()
			{
				BufferDesc desc = { 1, 1, 1, 0, BufferType::None, OptixDotNet::Format::Unknown };
				return desc;
			}
		}
	};

	/// <summary>
	/// A multi-dimensional array used to pass data between the host and an OptiX device, 
	/// that can be bound to <see cref="OptixDotNet::Variable">Variables</see> and <see cref="OptixDotNet::TextureSampler">TextureSamplers</see>.
	/// </summary>
	public ref class Buffer : public DataNode
	{
	public:

		/// <summary>
		/// Creates a 1/2/3D optix buffer based on the input BufferDesc.
        /// </summary>
        /// <param name="context">Optix context</param>
		/// <param name="desc">Description of the buffer</param>
		Buffer( Context^ context, BufferDesc desc );

		/// <summary>
		/// Checks to make sure the object is valid and consistent
        /// </summary>
		virtual void Validate() override
		{
			CheckError( rtBufferValidate( mBuffer ) );
		}

		/// <summary>
		/// Destroys the internal Optix resource
        /// </summary>
		virtual void Destroy() override
		{
			if( mBuffer )
				CheckError( rtBufferDestroy( mBuffer ) );

			mBuffer = 0;
		}

		/// <summary>
		/// Copies the data from the provided array into the Buffer. [data] must be non-null.
        /// </summary>
        /// <param name="data">Data array that provides the data</param>
		generic< typename T > where T : value class
		void SetData( array<T>^ data )
		{
			if( ( mType & BufferType::Input ) != BufferType::Input )
				throw gcnew System::Exception( "Buffer Error: Attempting to set data on a non Input buffer!" );

			if( data == nullptr || data->Length == 0)
				return;		

			RTsize mySize;
			CheckError( rtBufferGetElementSize( mBuffer, &mySize ) );

			mySize *= mWidth;
			if( mHeight > 0 )
				mySize *= mHeight;
			if( mDepth > 0 )
				mySize *= mDepth;

			RTsize size = data->Length * sizeof( T );

			if( size > mySize )
				throw gcnew System::Exception( "Buffer Error: Attempting to set data that is larger than the buffer size" );

			pin_ptr<T> dataPtr = &data[0];

			void* bufferData = 0;
			CheckError( rtBufferMap( mBuffer, (void**)&bufferData ) );

			if( bufferData == 0 )
				throw gcnew System::Exception( "Buffer Error: Internal buffer cannot be mapped" );

			memcpy( bufferData, dataPtr, size );

			CheckError( rtBufferUnmap( mBuffer ) );
		}

		/// <summary>
		/// Gets the data from the buffer and copies it into [data]. If [data] is null, it will be created with N * width * height elements;
		/// Where N = sizeof of buffer data elem / sizeof T. This allows a float array to be passed in for float3 data.
        /// </summary>
        /// <param name="data">Data array to copy into</param>
		generic< typename T > where T : value class
		void GetData( array<T>^% data )
		{
			if( ( mType & BufferType::Output ) != BufferType::Output )
				throw gcnew System::Exception( "Buffer Error: Attempting to get data on a non Output buffer!" );

			RTsize size;
			RTsize tSize = sizeof( T );

			RTsize length = mWidth;
			if( mHeight > 0 )
				length *= mHeight;
			if( mDepth > 0 )
				length *= mDepth;

			CheckError( rtBufferGetElementSize( mBuffer, &size ) );

			if( tSize != size )
				throw gcnew System::Exception( "Buffer Error: Array Type is not of equal size to Buffer Format type." );

			if( data == nullptr )
				data = gcnew array<T>( length * tSize );
			
			size *= length;
			
			pin_ptr<T> dataPtr = &data[0];

			void* bufferData = 0;
			CheckError( rtBufferMap( mBuffer, (void**)&bufferData ) );

			if( bufferData == 0 )
				throw gcnew System::Exception( "Buffer Error: Internal buffer cannot be mapped" );

			memcpy( dataPtr, bufferData, size );

			CheckError( rtBufferUnmap( mBuffer ) );
		}

		/// <summary>
		/// Gets the data from the buffer and copies it into [data]. Data must be an already alloced buffer of the correct size.
        /// </summary>
        /// <param name="data">Data array to copy into</param>
		generic< typename T > where T : value class
		void GetDataNoAlloc( array<T>^ data )
		{
			if( ( mType & BufferType::Output ) != BufferType::Output )
				throw gcnew System::Exception( "Buffer Error: Attempting to get data on a non Output buffer!" );

			if( data == nullptr )
				throw gcnew System::ArgumentNullException( "data" );

			RTsize size;
			CheckError( rtBufferGetElementSize( mBuffer, &size ) );

			size *= mWidth;
			if( mHeight > 0 )
				size *= mHeight;
			if( mDepth > 0 )
				size *= mDepth;

			if( ( sizeof(T) * data->Length ) < size )
				throw gcnew System::Exception( "Buffer Error: data not size not enough to copy buffer data!" );

			void* bufferData = 0;
			pin_ptr<T> dataPtr = &data[0];

			CheckError( rtBufferMap( mBuffer, (void**)&bufferData ) );

			if( bufferData == 0 )
				throw gcnew System::Exception( "Buffer Error: Internal buffer cannot be mapped" );

			memcpy( dataPtr, bufferData, size );

			CheckError( rtBufferUnmap( mBuffer ) );
		}

		/// <summary>
		/// Maps the internal buffer for reading/writing. Must be paired with an Unmap call.
        /// </summary>
		BufferStream^ Map()
		{
			RTsize size;
			CheckError( rtBufferGetElementSize( mBuffer, &size ) );

			void* bufferData = 0;
			CheckError( rtBufferMap( mBuffer, (void**)&bufferData ) );

			if( bufferData == 0 )
				throw gcnew System::Exception( "Buffer Error: Internal buffer cannot be mapped" );

			bool canRead = ( mType & BufferType::Output ) == BufferType::Output;
			bool canWrite = ( mType & BufferType::Input ) == BufferType::Input;

			size *= mWidth;
			if( mHeight > 0 )
				size *= mHeight;
			if( mDepth > 0 )
				size *= mDepth;

			return gcnew BufferStream( bufferData, size, canRead, canWrite, false );
		}

		/// <summary>
		/// Unmaps the internal buffer from reading/writing. Must be paired with an Map call.
        /// </summary>
		void Unmap()
		{
			CheckError( rtBufferUnmap( mBuffer ) );
		}

		/// <summary>
		/// Gets the BufferType the Buffer was created with
        /// </summary>
		property BufferType Type
		{
			BufferType get() { return mType; }
		}

		/// <summary>
		/// Gets the Format the Buffer was created with
        /// </summary>
		property OptixDotNet::Format Format
		{
			OptixDotNet::Format get() { return mFormat; }
		}

		/// <summary>
		/// Gets the width of the Buffer
        /// </summary>
		property Size Width
		{
			Size get() { return mWidth; }
		}

		/// <summary>
		/// Gets the height of the Buffer
        /// </summary>
		property Size Height
		{
			Size get() { return mHeight; }
		}

		/// <summary>
		/// Gets the depth of the Buffer
        /// </summary>
		property Size Depth
		{
			Size get() { return mDepth; }
		}

		/// <summary>
		/// Gets size of an element in the buffer
        /// </summary>
		property Size ElementSize
		{
			Size get()
			{
				Size size;
				CheckError( rtBufferGetElementSize( mBuffer, &size ) );
				return size;
			}
		}

	protected:
		void Create( BufferDesc% desc );
		void SetSize( Size width, Size height, Size depth );
	internal:
		Buffer( Context^ context );

		inline RTbuffer InternalPtr() { return mBuffer; }
		virtual RTobject ObjectPtr() override { return InternalPtr(); }

	protected:

		RTbuffer			mBuffer;

		Size				mWidth;
		Size				mHeight;
		Size				mDepth;
		OptixDotNet::Format	mFormat;
		BufferType			mType;
	};
}